
package wal

import (
	"fmt"
	"os"
	"path/filepath"

	"go.etcd.io/etcd/client/pkg/v3/fileutil"

	"go.uber.org/zap"
)

// 负责预创建日志文件并为日志文件预分配空间。
// 在filePipeline中会启动一个独立的后台goroutine来创建＂.tmp”结尾的临时文件，
// 当进行日志文件切换时，直接将临时文件进行重命名即可使用
type filePipeline struct {
	lg *zap.Logger
	// 存放临时文件的目录。
	dir string
	// 创建临时文件时预分配空间的大小，默认是64MBC由wal.SegmentSizeBytes指定，该值也是每个日志文件的大小）。
	size int64
	// 当前fil巳Pipeline实例创建的临时文件数。
	count int
	// 新建的临时文件句柄会通过filec通道返回给WAL实例使用。
	filec chan *fileutil.LockedFile
	// 当创建临时文件出现异常时，则将异常传递到errc通道中。
	errc  chan error
	// 当filePipeline.Close（）被调用时会关闭donee通道，
	// 从而通知filePipeline实例删除最后一次创建的临时文件。
	donec chan struct{}
}

// newFilePipeline（）方法中，除了创建filePipeline实例，
// 还会启动一个后台goroutine来执行filePipeline.run（）方法，
// 该后台goroutine中会创建新的临时文件并将其句柄传递到filec通道中。
func newFilePipeline(lg *zap.Logger, dir string, fileSize int64) *filePipeline {
	if lg == nil {
		lg = zap.NewNop()
	}
	fp := &filePipeline{
		lg:    lg,
		dir:   dir,
		size:  fileSize,
		filec: make(chan *fileutil.LockedFile),
		errc:  make(chan error, 1),
		donec: make(chan struct{}),
	}
	go fp.run()
	return fp
}

// 在WAL切换日志文件时会调用filePipeline.Open（）方法，
//从filec通道中获取之前创建好的临时文件，
//
// Open返回一个新的文件以供写入。在调用
// 重新打开之前重命名文件，否则会发生文件冲突。
func (fp *filePipeline) Open() (f *fileutil.LockedFile, err error) {
	select {
	case f = <-fp.filec:	// 从filec远远中获取已经创建好的临时文件，并返回
	case err = <-fp.errc:	// 如采创建临时文件时的异常，则通过errc通道中获取，并返回
	}
	return f, err
}

func (fp *filePipeline) Close() error {
	close(fp.donec)
	return <-fp.errc
}

// 创建临时文件
func (fp *filePipeline) alloc() (f *fileutil.LockedFile, err error) {
	// 为了防止与前一个创建的临时文件重名，新建临时文件的编号是0或是1
	fpath := filepath.Join(fp.dir, fmt.Sprintf("%d.tmp", fp.count%2))
	// 创建临时文件，注意义件的模式和权限
	if f, err = fileutil.LockFile(fpath, os.O_CREATE|os.O_WRONLY,
		fileutil.PrivateFileMode); err != nil {
		return nil, err
	}
	// 尝试预分自己空间，如果当前系统不支持预分自己文件空间，则并不会报错
	if err = fileutil.Preallocate(f.File, fp.size, true); err != nil {
		fp.lg.Error("failed to preallocate space when creating a new WAL", zap.Int64("size", fp.size), zap.Error(err))
		f.Close()	// 如果出现异常，则会关闭donec 通道
		return nil, err
	}
	fp.count++	// 递增创建的文件数量
	return f, nil	// 返回创建的临时文件
}

// filePipeline.run（）方法，该后台goroutine中会创建新的临时文件并将其句柄传递到日lee通道中。
func (fp *filePipeline) run() {
	defer close(fp.errc)
	for {
		f, err := fp.alloc()	// 调用alloc（）方法创建临时文件
		// 如果创建临时文件失败，则将异常传递到errc通道中
		if err != nil {
			fp.errc <- err
			return
		}
		select {
		case fp.filec <- f:	// 将上面创建的临时文件句柄传递到filec通道中
		case <-fp.donec:	// 关闭时触发，删除最后一次创建的临时文件
			os.Remove(f.Name())
			f.Close()
			return
		}
	}
}
